#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
    Part of Seagull - The Pyhon Pages engine
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    Python Page Compiler -- Parser
"""
__author__ = 'Jonathan Slenders <jonathan at slenders dot be>'
__license__ = 'GNU LGPL'
__all__ = ('Parser', )


# TODO: this is work in progress
# I want to become this
# - lexer generates lex tree
# - parser inserts tree in a usable data structure
# - parse_tree_manipulations works on this usable structure
# - writeback starts from parser data structure


# In this data structure:
# - None means 'unspecified', it's an absent setting in the source code file
#   (and so, it will mean the same as the default value)

import lexer

from compile_exceptions import CompileException

class PythonPage(object):
	"""
	Contains the whole Python Page
	"""
	def __init__(self):
		self.meta = [ ] # Array of Meta
		self.parameters = [ ] # Array of PageParameter
		self.output_mode = None
		self.content_type = None
		self.whitespace_compression = None
		self.external = None
		self.disabled = None
		self.master = None
		self.filters = [ ] # Array of Filter

		self.init_block = ContentBlock('')
		self.content_blocks = [ ] # Array of ContentBlock

class Meta(object):
	"""
	Meta tag {meta name="..." value="..." }
	"""
	def __init__(self, name, value):
		self.name = name
		self.value = value

class Master(object):
	"""
	Master tag {master path="..." ~param="..." ~param2="..." }
	"""
	def __init__(self, path, parameters = { }):
		self.path = path
		self.parameters = parameters # key:str -> value:str dict

class Filter(object):
	"""
	Filter tag {filter name="..." ~param="..." ~param2="..." }
	"""
	def __init__(self, name, parameters = { }):
		self.name = name
		self.parameters = parameters # key:str -> value:str dict

class PageParameter(object):
	"""
	Page Parameter tag
	{param name="..." type="query-string|read-only|set-once|read-write" class="object|int|..." }
	"""
	def __init__(self, name, classname=None, description=None, default_value=None, type=None, get_function=None, set_function=None):
		self.name = name
		self.type = type
		self.classname = classname
		self.description = description
		self.default_value = default_value
		self.get_function = get_function
		self.set_function = set_function

class ContentBlock(object):
	"""
	Content block tag with following content
	{content name="..." ~param="value" }
	"""
	class Comment(object):
		def __init__(self, content):
			self.content = content

	def __init__(self, name=None, skip_whitespace_after_declaration=False):
		self.skip_whitespace_after_declaration = skip_whitespace_after_declaration
		self.name = name
		self.content = [ ] # Mix of unicode, EmbeddedPage, Placeholder, Call, Python and Comment
		self.parameters = { }

class Placeholder(object):
	"""
	Content placeholder
	{placeholder name="..." }
	"""
	def __init__(self, name, skip_whitespace_after_declaration=False):
		self.name = name
		self.skip_whitespace_after_declaration = skip_whitespace_after_declaration

class Call(object):
	"""
	Content block call
	{call entry="..." }
	"""
	def __init__(self, entry, skip_whitespace_after_declaration=False):
		self.entry = entry
		self.skip_whitespace_after_declaration = skip_whitespace_after_declaration

class EmbeddedPage(object):
	"""
	Embedded page
	{"/path" p="..." ~param="value" }
	"""
	def __init__(self, path, parameters={ }, object_varname=None, params_varname=None, render=None, entry=None, skip_whitespace_after_declaration=False):
		self.path = path
		self.object_varname = object_varname
		self.params_varname = params_varname
		self.render = render
		self.entry = entry # entry-point:str
		self.parameters = parameters # key:str -> value:str dict
		self.default_content_block = None # ContentBlock
		self.content_blocks = [ ] # Array of ContentBlock
		self.skip_whitespace_after_declaration = skip_whitespace_after_declaration
		self.skip_whitespace_after_end_declaration=False # In {] %/>

class PythonCode(object):
	"""
	Python code (contains only python-pages code syntax, including 'endif', 'enddef',... tags)
	{ ... }
	"""
	class Position(object):
		"""
		Remebers line/column position in source file
		(required for inserting debug interrupts later on)
		"""
		def __init__(self, line=0, column=0):
			self.line = line
			self.column = column

	class Comment(Position):
		"""
		Single line Python comment
		"""
		def __init__(self, content, line=0, column=0):
			PythonCode.Position.__init__(self, line, column)
			self.content = content

	class String(Position):
		"""
		Python string
		"""
		def __init__(self, content, quote_type='single-quoted', line=0, column=0):
			# IMPORTANT NOTE: the 'content' is SUPPOSED to be ESCAPED (with backslashes)
			#   normally, strings coming from the lex tree are already escaped
			PythonCode.Position.__init__(self, line, column)
			self.quote_type = quote_type
			self.content = content

	class Code(Position):
		"""
		Other python code
		"""
		def __init__(self, content, line=0, column=0):
			PythonCode.Position.__init__(self, line, column)
			self.content = content

	def __init__(self, print_result_escaped=False, skip_whitespace_after_declaration=False):
		self.print_result_escaped = print_result_escaped
		self.code = [ ]  # Array of Code (may contain Comment, String and Code)
		self.skip_whitespace_after_declaration = skip_whitespace_after_declaration


class Parser(object):
	def __init__(self, lex_tree):
		assert isinstance(lex_tree, lexer.Token) and lex_tree.name == 'root'

		self.python_page = PythonPage()
		self._block_stack = [ self.python_page.init_block ] # Append content here
		self._embedded_page_stack = [ ] # Current embedded page, where to insert new ContentBlocks

		# Parse
		for t in lex_tree.children:
			if isinstance(t, basestring):
				self._block_stack[ -1 ].content.append(t)

			elif t.name in ('python', 'python-print-escaped'):
				escaped = (t.name == 'python-print-escaped')
				if escaped and self._block_stack[ -1 ] == self.python_page.init_block:
						raise CompileException(t.line, t.column,
								'Print statements are not allowed in the initialisation block')

				python = PythonCode(print_result_escaped=escaped)
				self._block_stack[ -1 ].content.append(python)

				for c in t.children:
					try:
						if isinstance(c, basestring):
							python.code.append(PythonCode.Code(c))

						elif c.name == 'string':
							python.code.append(PythonCode.String(c.get_string_value(),
									quote_type=c.attributes['stringtype'], line=c.line, column=c.column))

						elif c.name == 'semi-colon':
							python.code.append(PythonCode.Code(';', c.line, c.column))

						elif c.name == 'colon':
							python.code.append(PythonCode.Code(':', c.line, c.column))

						elif c.name == 'single-line-comment':
							python.code.append(PythonCode.Comment(c.get_string_value(), c.line, c.column))

						elif c.name == 'python-whitespace':
							python.code.append(PythonCode.Code(c.get_string_value(), c.line, c.column))

						else: raise c.name
					except CompileException, e:
						raise CompileException(t.line, t.column, e.message)

			elif t.name == 'specialtag-page':
				self._process_page_tag(t)

			elif t.name == 'specialtag-master':
				self._process_page_master(t)

			elif t.name == 'specialtag-parameter':
				self._process_page_parameter(t)

			elif t.name == 'specialtag-meta':
				self._process_meta_tag(t)

			elif t.name == 'specialtag-filter':
				self._process_filter_tag(t)

			elif t.name == 'specialtag-content':
				self._process_content_start(t)
			
			elif t.name == 'specialtag-call':
				self._process_call_tag(t)

			elif t.name in ('start-embedded-object', 'end-embedded-object', 'embedded-object'):
				if self._block_stack[ -1 ] != self.python_page.init_block:
					if t.name == 'start-embedded-object':
						self._process_embedded_start(t)

					elif t.name == 'end-embedded-object':
						self._process_embedded_end(t)
						
					elif t.name == 'embedded-object':
						self._process_included_page_tag(t)
					else: raise
				else:
					raise CompileException(t.line, t.column,
						'Embedding pages is not allowed in the initialisation block')


			elif t.name == 'specialtag-placeholder':
				if self._block_stack[ -1 ] != self.python_page.init_block:
					self._process_placeholder_tag(t)
				else:
					raise CompileException(t.line, t.column,
						'Placeholder tags are not allowed in the initialisation block')

			elif t.name == 'xml-comment':
				self._block_stack[ -1 ].content.append(ContentBlock.Comment(t.get_string_value()))
			else: raise t.name

		# Post checks
		if len(self._block_stack) > 1:
			raise CompileException(0,0, 'Embedded page tag {[... has not been closed')

		if self.python_page.output_mode and self.python_page.master:
			raise CompileException(0,0, 'Page cannot define both output mode and master') 

		if self.python_page.content_type and self.python_page.master:
			raise CompileException(0,0, 'Page cannot define both content type and master') 

	def get_result(self):
		return self.python_page

	def _parse_special_tag(self, tag):
		"""
		Yields key, value pairs for tags like {specialtag key="value" }
		"""
		key = ''
		
		for t in tag.children:
			if t.name == 'specialtag-param-key':
				key = t.get_string_value()

			elif t.name == 'specialtag-param-value':
				yield key, t.get_string_value()
				key = ''

	def _skip_whitespace_after_declaration(self, tag):
		"""
		Return true when tag ends in %/>
		"""
		return 'skipwhitespace' in tag.attributes and tag.attributes['skipwhitespace']

	def _process_content_start(self, tag):
		"""
		Parse {content name="..." ~param="..." }
		"""
		name = None
		key = ''
		parameters = { }

		for key, value in self._parse_special_tag(tag):
			if key == 'name':
				name = value

			elif key[0:1] =='~':
				parameters[key[1:]] = value
			else:
				raise CompileException(tag.line, tag.column, 'Unknown attribute "%s"' % key)


		block = ContentBlock(name, skip_whitespace_after_declaration=self._skip_whitespace_after_declaration(tag))
		block.parameters = parameters

		if len(self._block_stack) == 1:
			self.python_page.content_blocks.append(block)
		else:
			self._embedded_page_stack [ -1 ].content_blocks.append(block)

		self._block_stack[ -1 ] = block

	def _process_page_tag(self, tag):
		"""
		Parse {page ... }
		"""
		for key, value in self._parse_special_tag(tag):
			if key == 'content-type':
				self.python_page.content_type = value

			elif key == 'compress-whitespace':
				if not value in ('true', 'false'):
					raise CompileException(tag.line, tag.column,
						'Compress-whitespace should be either "true" or "false"')
				self.python_page.whitespace_compression = (value == 'true')

			elif key == 'external':
				if not value in ('true', 'false'):
					raise CompileException(tag.line, tag.column,
						'External should be either "true" or "false"')

				self.python_page.external = (value == 'true')

			elif key == 'output':
				if not value in ('xhtml', 'atom', 'raw'):
					raise CompileException(tag.line, tag.column,
						'Output should be "xhtml", "atom" or "raw"')

				self.python_page.output_mode = value

			elif key == 'disabled':
				if not value in ('true', 'false'):
					raise CompileException(tag.line, tag.column,
						'Disabled should be either "true" or "false"')

				self.python_page.disabled = (value == 'true')

			else:
				raise CompileException(tag.line, tag.column, 'Unknown attribute "%s"' % key)

	def _process_page_master(self, tag):
		"""
		Parse {master path="..." ~param="..." }
		"""
		path = None
		parameters = { }
		for key, value in self._parse_special_tag(tag):
			if key == 'path':
				path = value

			elif key[0:1] == '~':
				parameters[key[1:]] = value

			else:
				raise CompileException(tag.line, tag.column,
					'Unknown attribute "%s"' % key)

		# Path attribute is required
		if path == None:
			raise CompileException(tag.line, tag.column, 'Path not defined for master page')

		self.python_page.master = Master(path, parameters)

	def _process_page_parameter(self, tag):
		"""
		Parse {param name="..." default-value="..." class="..." type="..." description="..." }
		"""
		name = None
		classname = 'string'
		description = None
		default_value = None
		get_function = None
		set_function = None
		type = 'query-string'
		key = ''

		for key, value in self._parse_special_tag(tag):
			if key == 'name':
				name = value

			elif key == 'class':
				if not value in ('string', 'bool', 'int', 'float', 'object', 'function'):
					raise CompileException(tag.line, tag.column,
						'Class should be "string", "bool", "int", "float", "object" or "function"')

				classname = value

			elif key == 'default-value':
				default_value = value

			elif key == 'type':
				if not value in ('query-string', 'set-once', 'read-write', 'read-only'):
					raise CompileException(tag.line, tag.column,
						'Type should be "query-string", "set-once", "read-write" or "read-only"')

				type = value

			elif key == 'get':
				get_function = value

			elif key == 'set':
				set_function = value

			elif key == 'description':
				description = value

			else:
				raise CompileException(tag.line, tag.column,
					'Unknown attribute "%s"' % key)
					
		# Required attributes
		if name == None:
			raise CompileException(tag.line, tag.column, 'Paramater tag should have a name')

		# Check for appropriate get/set functions according to parametr type
		# NOTE: read-write functions can have get/set but this isn't required
		if type == 'query-string' and (set_function or get_function):
			raise CompileException(tag.line, tag.column, 'Query string parameter "%s" can\'t have get or set functions' % name)
		if type == 'set-once' and (set_function or get_function):
			raise CompileException(tag.line, tag.column, 'Set-once parameter "%s" can\'t have get or set functions' % name)
		if type == 'read-only' and (set_function or not get_function):
			raise CompileException(tag.line, tag.column, 'Read-only parameter "%s" should have a get function, but can\'t have a set function' % name)

		# Every set requires a get
		if set_function and not get_function:
			raise CompileException(tag.line, tag.column, 'Paramater "%s" should also have a get_function (because set has been defined)' % name)

		# default-value cannot be combined with get/set functions
		if (set_function or get_function) and default_value:
			raise CompileException(tag.line, tag.column, 'Paramater "%s" cannot have a default-value when a set or get function has been defined' % name)

		# Query string parameters should have 'string'-type
		if type == 'query-string' and classname != 'string':
			raise CompileException(tag.line, tag.column, 'Query string paramater "%s" should have the "string"-class.' % name)

		# Remember parameter
		self.python_page.parameters.append(PageParameter(name, classname, description, default_value, type, get_function, set_function))

	def _process_meta_tag(self, tag):
		"""
		Parse {meta name="..." value="..."
		"""
		name = None
		value = None

		for key, value in self._parse_special_tag(tag):
			if key == 'name':
				name = value
			elif key == 'value':
				value = value
			else:
				raise CompileException(tag.line, tag.column, 'Unknown meta attribute "%s"' % key)
					
		if name == None or value == None:
			raise CompileException(tag.line, tag.column, 'Meta tag should have a both a name and a value')

		self.python_page.meta.append(Meta(name, value))

	def _process_filter_tag(self, tag):
		"""
		Parse {filter name="..." ~param="value" ... }
		"""
		name = None
		parameters = { }

		for key, value in self._parse_special_tag(tag):
			if key == 'name':
				name = value
			elif key[0:1] == '~':
				parameters[key[1:]] = value
			else:
				raise CompileException(tag.line, tag.column, 'Unknown filter attribute "%s"' % key)
					
		if name == None:
			raise CompileException(tag.line, tag.column, 'Filter tag should have at least a name')

		self.python_page.filters.append(Filter(name, parameters))
	
	def _process_placeholder_tag(self, tag):
		"""
		Parse {placeholder name="..." }
		"""
		name = None

		for key, value in self._parse_special_tag(tag):
			if key == 'name':
				name = value

			elif key == 'required':
				print '"Required"-keyword is OBSOLETE'
			else:
				raise CompileException(tag.line, tag.column, 'Unknown attribute "%s"' % key)

		self._block_stack[ -1 ].content.append(Placeholder(name,
				skip_whitespace_after_declaration=self._skip_whitespace_after_declaration(tag)))

	def _process_call_tag(self, tag):
		"""
		Parse {call entry="..." }
		"""
		entry = None
		for key, value in self._parse_special_tag(tag):
			if key == 'entry':
				entry = value

			else:
				raise CompileException(tag.line, tag.column, 'Unknown attribute "%s"' % key)

		if entry == None:
			raise CompileException(tag.line, tag.column, 'Call tag should have an entry')

		self._block_stack[ -1 ].content.append(Call(entry,
				skip_whitespace_after_declaration=self._skip_whitespace_after_declaration(tag)))

	def _process_included_page_tag(self, tag):
		"""
		Parse {"..." p="..." o="..." ~param="..." ... }
		"""
		embedded_page = self._process_embedded_page_tag_parameters(tag)
		self._block_stack[ -1 ].content.append(embedded_page)

	def _process_embedded_start(self, tag):
		"""
		Parse {<"..." p="..." o="..." ~param="..." ... }
		"""
		# New page
		embedded_page = self._process_embedded_page_tag_parameters(tag)
		default_block = ContentBlock('default')
		embedded_page.default_content_block = default_block

		# Add new embedded page to parent block
		self._block_stack[ -1 ].content.append(embedded_page)

		# Add to stacks
		self._embedded_page_stack.append(embedded_page)
		self._block_stack.append(default_block)

	def _process_embedded_end(self, tag):
		"""
		End of embeddded page, Parse {> }
		"""
		# Pop from block stack (never pop last item)
		if len(self._block_stack) > 1:
			# In case of {] %\>
			self._embedded_page_stack[ - 1 ].skip_whitespace_after_end_declaration=self._skip_whitespace_after_declaration(tag)

			# Pop stack
			self._block_stack.pop()
			self._embedded_page_stack.pop()
		else:
			raise CompileException(tag.line, tag.column, '{> } found without opening tag')

	def _process_embedded_page_tag_parameters(self, tag):
		"""
		Parse {<"..." p="..." o="..." render="false" ~param="..." ... }
		and {"..." p="..." o="..." render="false" ~param="..." ... }
		"""
		path = None
		render = None
		entry = None
		object_varname = None
		params_dict_varname = None
		parameters = { } # Starting with a tilde ~

		for t in tag.children:
			if t.name == 'include-path':
				path = t.get_string_value()

		for key, value in self._parse_special_tag(tag):
			if key[0:1] == '~':
				parameters[key[1:]] = value

			elif key == 'o': # Object varname
				object_varname = value

			elif key == 'p': # Params dictionary varname
				params_dict_varname = value

			elif key == 'render':
				if not value in ('true', 'false'):
					raise CompileException(tag.line, tag.column,
							'Render parameter for embedded page should be either "true" or "false"')
				else:
					render = (value == 'true')

			elif key == 'entry':
				entry = value

			else:
				raise CompileException(tag.line, tag.column, 'Unknown parameter "%s"' % key) 

		return EmbeddedPage(path, parameters, object_varname, params_dict_varname, render, entry,
				skip_whitespace_after_declaration=self._skip_whitespace_after_declaration(tag))

